package org.easy4j.framework.web.startup;

import org.easy4j.framework.core.config.Config;
import org.easy4j.framework.core.plugin.Plugin;
import org.easy4j.framework.core.plugin.ScanPackageHelper;
import org.easy4j.framework.web.startup.config.WebMvcConfig;
import org.springframework.core.Conventions;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.*;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.ServiceLoader;
import java.util.Set;

/**
 * @author bjliuyong
 * @version 1.0
 * @created date 16-10-11
 */
public class DispatcherServletContainerInitializer extends ContextLoader implements ServletContainerInitializer {

    /**
     * The default servlet name. Can be customized by overriding {@link #getServletName}.
     */
    public static final String DEFAULT_SERVLET_NAME = "dispatcher";

    static {

        ServiceLoader<Plugin> plugins = ServiceLoader.load(Plugin.class);
        Iterator<Plugin> pluginIterator = plugins.iterator();
        for( ; pluginIterator.hasNext() ; ) {
            Plugin plugin = pluginIterator.next() ;
            plugin.init();
        }
    }

    public DispatcherServletContainerInitializer(){
        super(ApplicationContextUtils.createRootApplicationContext());
    }

    @Override
    public void onStartup(Set<Class<?>> c, ServletContext servletContext) throws ServletException {

        initWebApplicationContext(servletContext);

        registerDispatcherServlet(servletContext);
    }

    /**
     * Register a {@link org.springframework.web.servlet.DispatcherServlet} against the given servlet context.
     * <p>This method will create a {@code DispatcherServlet} with the name returned by
     * {@link #getServletName()}, initializing it with the application context returned
     * from {@link #createServletApplicationContext()}, and mapping it to the patterns
     * returned from {@link #getServletMappings()}.
     * <p>Further customization can be achieved by overriding {@link
     * #customizeRegistration(javax.servlet.ServletRegistration.Dynamic)}.
     * @param servletContext the context to register the servlet against
     */
    protected void registerDispatcherServlet(ServletContext servletContext) {
        String servletName = getServletName();
        Assert.hasLength(servletName, "getServletName() must not return empty or null");

        WebApplicationContext servletAppContext = createServletApplicationContext();
        Assert.notNull(servletAppContext,
                "createServletApplicationContext() did not return an application " +
                        "context for servlet [" + servletName + "]");

        DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext);
        ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
        Assert.notNull(registration,
                "Failed to register servlet with name '" + servletName + "'." +
                        "Check if there is another servlet registered under the same name.");

        registration.setLoadOnStartup(1);
        registration.addMapping(getServletMappings());
        registration.setAsyncSupported(isAsyncSupported());

        Filter[] filters = getServletFilters();
        if (!ObjectUtils.isEmpty(filters)) {
            for (Filter filter : filters) {
                registerServletFilter(servletContext, filter);
            }
        }

        customizeRegistration(registration);
    }


    /**
     * {@inheritDoc}
     * <p>This implementation creates an {@link org.springframework.web.context.support.AnnotationConfigWebApplicationContext},
     * providing it the annotated classes returned by {@link #getServletConfigClasses()}.
     */
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext servletAppContext = new AnnotationConfigWebApplicationContext();

        servletAppContext.scan(ScanPackageHelper.getMvcPackages());

        Class<?>[] configClasses = getServletConfigClasses();
        if (!ObjectUtils.isEmpty(configClasses)) {
            servletAppContext.register(configClasses);
        }
        return servletAppContext;
    }

    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{WebMvcConfig.class};
    }

    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * Specify filters to add and map to the {@code DispatcherServlet}.
     * @return an array of filters or {@code null}
     * @see #registerServletFilter(ServletContext, javax.servlet.Filter)
     */
    protected Filter[] getServletFilters() {

        CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
        encodingFilter.setEncoding(Config.CHARSET_NAME);

        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();

        return new Filter[]{encodingFilter ,hiddenHttpMethodFilter};
    }








    /**
     * Add the given filter to the ServletContext and map it to the
     * {@code DispatcherServlet} as follows:
     * <ul>
     * <li>a default filter name is chosen based on its concrete type
     * <li>the {@code asyncSupported} flag is set depending on the
     * return value of {@link #isAsyncSupported() asyncSupported}
     * <li>a filter mapping is created with dispatcher types {@code REQUEST},
     * {@code FORWARD}, {@code INCLUDE}, and conditionally {@code ASYNC} depending
     * on the return value of {@link #isAsyncSupported() asyncSupported}
     * </ul>
     * <p>If the above defaults are not suitable or insufficient, override this
     * method and register filters directly with the {@code ServletContext}.
     * @param servletContext the servlet context to register filters with
     * @param filter the filter to be registered
     * @return the filter registration
     */
    protected FilterRegistration.Dynamic registerServletFilter(ServletContext servletContext, Filter filter) {
        String filterName = Conventions.getVariableName(filter);
        FilterRegistration.Dynamic registration = servletContext.addFilter(filterName, filter);
        if (registration == null) {
            int counter = -1;
            while (counter == -1 || registration == null) {
                counter++;
                registration = servletContext.addFilter(filterName + "#" + counter, filter);
                Assert.isTrue(counter < 100,
                        "Failed to register filter '" + filter + "'." +
                                "Could the same Filter instance have been registered already?");
            }
        }
        registration.setAsyncSupported(isAsyncSupported());
        registration.addMappingForServletNames(getDispatcherTypes(), false, getServletName());
        return registration;
    }

    private EnumSet<DispatcherType> getDispatcherTypes() {
        return (isAsyncSupported() ?
                EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ASYNC) :
                EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE));
    }

    /**
     * Return the name under which the {@link DispatcherServlet} will be registered.
     * Defaults to {@link #DEFAULT_SERVLET_NAME}.
     * @see #registerDispatcherServlet(ServletContext)
     */
    protected String getServletName() {
        return DEFAULT_SERVLET_NAME;
    }


    /**
     * A single place to control the {@code asyncSupported} flag for the
     * {@code DispatcherServlet} and all filters added via {@link #getServletFilters()}.
     * <p>The default value is "true".
     */
    protected boolean isAsyncSupported() {
        return true;
    }

    /**
     * Optionally perform further registration customization once
     * {@link #registerDispatcherServlet(ServletContext)} has completed.
     * @param registration the {@code DispatcherServlet} registration to be customized
     * @see #registerDispatcherServlet(ServletContext)
     */
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    }
}
